package utilities

import (
	"mi/compiler/ast"
	"mi/compiler/diagnostic"
	"mi/compiler/scanner"
	"mi/internal/span"
	"unicode/utf8"
)

func assert(expr bool) {
	if !expr {
		panic("Assert Fail")
	}
}

func GetEndLinePosition(line int, sourceFile *ast.SourceFile) int {
	if line < 0 {
		panic("GetEndLinePosition argument line < 0")
	}

	var lineStarts = scanner.GetLineStarts(sourceFile)

	var lineIndex = line
	var sourceText = sourceFile.Text
	if lineIndex+1 == len(lineStarts) {
		// last line - return EOF
		return len(sourceText) - 1
	} else {
		// current line start
		var start = lineStarts[lineIndex]
		// take the start position of the next line - 1 = it should be some line break
		var pos = lineStarts[lineIndex+1] - 1
		ch, size := utf8.DecodeRune(sourceText[pos:])
		assert(scanner.IsLineBreak(ch))
		// walk backwards skipping line breaks, stop the the beginning of current line.
		// i.e:
		// <some text>
		// $ <- end of line for this position should match the start position
		for start <= pos && scanner.IsLineBreak(ch) {
			pos -= size
			ch, size = utf8.DecodeRune(sourceText[pos:])
		}
		return pos
	}
}

func CreateDiagnosticForNode(node ast.Node, message *diagnostic.Message, args ... interface{}) *diagnostic.Diagnostic {
	var sourceFile = ast.GetSourceFileOfNode(node)
	return CreateDiagnosticForNodeInSourceFile(sourceFile, node, message, args ... )
}

func CreateDiagnosticForNodeInSourceFile(sourceFile *ast.SourceFile, node ast.Node, message *diagnostic.Message, args ... interface{}) *diagnostic.Diagnostic {
	var span = GetErrorSpanForNode(sourceFile, node)
	return diagnostic.CreateFileDiagnostic(sourceFile, span.Start, span.Length, message, args...)
}

func CreateDiagnosticForNodeFromMessageChain(node ast.Node, messageChain *diagnostic.MessageChain) *diagnostic.Diagnostic {
	var sourceFile = ast.GetSourceFileOfNode(node)
	var span = GetErrorSpanForNode(sourceFile, node)

	if messageChain.Next != nil {
		return &diagnostic.Diagnostic{
			File:        sourceFile,
			Start:       span.Start,
			Length:      span.Length,
			MessageChain: messageChain,
			Category:    messageChain.Category,
			Code:        messageChain.Code,
		}
	} else {
		return &diagnostic.Diagnostic{
			File:        sourceFile,
			Start:       span.Start,
			Length:      span.Length,
			MessageText: messageChain.MessageText,
			Category:    messageChain.Category,
			Code:        messageChain.Code,
		}
	}
}

func GetSpanOfTokenAtPosition(sourceFile *ast.SourceFile, pos int) span.TextSpan {
	var s = scanner.NewScanner(func(opt *scanner.Optional) {
		opt.LanguageVersion = sourceFile.LanguageVersion
		opt.SkipTrivia = true
		opt.Text = sourceFile.Text
		opt.Start = pos
	})
	s.Scan()
	var start = s.GetTokenPos()
	return span.CreateTextSpanFromBounds(start, s.GetTextPos())
}

func getErrorSpanForArrowFunction(sourceFile *ast.SourceFile, node *ast.ArrowExpression) span.TextSpan {
	var pos = scanner.SkipTrivia(sourceFile.Text, node.Pos(), false, false)
	var body = node.Declaration.Body
	if body != nil && ast.IsBlockStatement(body) {
		var startLoc = scanner.GetFileLineAndCharacterFromPosition(sourceFile, body.Pos())
		var endLoc = scanner.GetFileLineAndCharacterFromPosition(sourceFile, body.End())
		if startLoc.Line < endLoc.Line {
			// The arrow function spans multiple lines,
			// make the error span be the first line, inclusive.
			return span.CreateTextSpan(pos, GetEndLinePosition(startLoc.Line, sourceFile)-pos+1)
		}
	}
	return span.CreateTextSpanFromBounds(pos, node.End())
}

func GetErrorSpanForNode(sourceFile *ast.SourceFile, node ast.Node) span.TextSpan {
	var errorNode = node

	switch n := node.(type) {
	case *ast.SourceFile:
		var pos = scanner.SkipTrivia(sourceFile.Text, 0, false, false)
		if pos == len(sourceFile.Text) {
			// file is empty - return span for the beginning of the file
			return span.CreateTextSpan(0, 0)
		}
		return GetSpanOfTokenAtPosition(sourceFile, pos)
		// This list is a work in progress. Add missing ast kinds to improve their error
		// spans.
	case *ast.VariableDeclaration,
		*ast.ClassDeclaration,
		*ast.InterfaceDeclaration,
		*ast.AnnotationDeclaration,
		*ast.EnumDeclaration,
		*ast.EnumItemDeclaration,
		*ast.MethodDeclaration:
		errorNode = ast.GetDeclarationName(n.(ast.Declaration))
	case *ast.ArrowExpression:
		return getErrorSpanForArrowFunction(sourceFile, n)
	case *ast.BodyDeclaration:
		switch pn := n.Parent().(type) {
		case *ast.EnumItemDeclaration:
			errorNode = pn.Name
		case *ast.NewObjectExpression:
			switch tpn := pn.Type.(type) {
			case *ast.QualifiedExpression:
				errorNode = tpn
			case *ast.ConstructorExpression:
				errorNode = tpn.TypeName
			}
		}
	}

	if isNull(errorNode) {
		// If we don't have a better ast, then just set the error on the first token of
		// construct.
		return GetSpanOfTokenAtPosition(sourceFile, node.Pos())
	}

	var pos int
	if ast.NodeIsMissing(errorNode) {
		pos = errorNode.Pos()
	} else {
		pos = scanner.SkipTrivia(sourceFile.Text, errorNode.Pos(), false, false)
	}

	return span.CreateTextSpanFromBounds(pos, errorNode.End())
}
